package hypeerweb;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;

import database.NodeProxyDB;

import proxyCommand.*;
import visitor.*;

/**
 * Holds all pertinent information about nodes.
 *
 */
public class Node implements Serializable {

	private static final long serialVersionUID = 1L;
	protected LocalObjectId localObjectId;
	private Contents contents;
	private static boolean migrating = false;
	
	/**
	 * The NULL_NODE using the NULL pattern.
	 */
	public static final Node NULL_NODE = new Node(-1, -1);

	/**
	 * the webId of the node.
	 */
	protected WebId webId;

	/**
	 * the connections of the node.
	 */
	protected Connections connections;

	/**
	 * The constructor for a node given only the new id.
	 *
	 * @param id
	 *            - the new id of the webId for this node.
	 * @pre id ≥ 0
	 * @post webId = new WebId(id) AND <br>
	 *       neighbors = new Neighbors() AND <br>
	 *       upPointers = new UpPointers() AND <br>
	 *       downPointers = new DownPointers() AND <br>
	 *       fold = this AND <br>
	 *       surrogateFold = NULL_NODE AND <br>
	 *       inverseSurrogateFold = NULL_NODE <br>
	 *
	 */
	public Node(int id) {
		localObjectId = new LocalObjectId();
		webId = new WebId(id);
		connections = new Connections(this);
		contents = new Contents();
		ObjectDB.getSingleton().store(localObjectId, this);
	}
	public Node(int id, LocalObjectId oldLocalObjectID){
		localObjectId = oldLocalObjectID;
		webId = new WebId(id);
		connections = new Connections(this);
		contents = new Contents();
		ObjectDB.getSingleton().store(localObjectId, this);
		
	}
	/**
	 * sets the nodes local object id
	 * @param lobj the new local object id
	 * @pre None
	 * @post the nodes local object id is set to lobj
	 */
	public void setLocalObjectId(LocalObjectId lobj){
		//we clear it out of where it was
		ObjectDB.getSingleton().remove(localObjectId);

		this.localObjectId=lobj;
		//and put it back in with its correct value now.
		ObjectDB.getSingleton().store(localObjectId, this);

	}
	public Node(){
		
	}

	/**
	 * @param id
	 *            - the new id of the webId for this node. <br>
	 *            height - the height of the webId for this node <br>
	 * @param height
	 * 			  - the height of the node
	 *
	 * @pre id ≥ 0 AND height ≤ 31 AND <br>
	 *      height ≥ WebId.locationOfMostSignificantOneBit(id) + 1 <br>
	 * @post webId = new WebId(id, height) AND <br>
	 *       neighbors = new Neighbors() AND <br>
	 *       upPointers = new UpPointers() AND <br>
	 *       downPointers = new DownPointers() AND <br>
	 *       fold = this AND <br>
	 *       surrogateFold = NULL_NODE AND <br>
	 *       inverseSurrogateFold = NULL_NODE <br>
	 */
	public Node(int id, int height) {
		webId = new WebId(id, height);
		connections = new Connections(this);
		contents = new Contents();
	}
	
	/**
	 *
	 * @param connectionspass
	 * 		-the set of connections which the node will have.
	 */
	public boolean setConnections(Connections connectionspass) {		
		connections = connectionspass;
		connections.setMyNode(this);
		return true;

	}
	/**
	 * @return the node's connections
	 *
	 * @pre connections must not be null <br>
	 * @post this nodes connections are returned.
	 */
	 public Connections getConnections() {
		return connections;
	 }
	 
	 public Connections getChildConnections(Node child) {
		 return connections.getChildConnections(child);
	 }

	/**
	 * Creates a simplified node domain to be used in testing.
	 *
	 * @return a simplified node domain with the integer
	 * 		   equivalent of the node's information.
	 *
	 * @pre <i>None</i>
	 * @post result.webId = webId.value AND <br>
	 *       height AND <br>
	 *       |neighbors| = |result.neighbors| AND<br>
	 *       ∀ n (neighbors.contains(n) ⇒
	 *       result.neighbors.contains(n.webId.value) AND <br>
	 *       |upPointers| = |result.upPointers| AND <br>
	 *       ∀ up (upPointers.contains(up) ⇒
	 *       result.upPointers.contains(up.webId.value) AND <br>
	 *       |downPointers| = |result.downPointers| AND <br>
	 *       ∀ downPointer (downPointers.contains(downPointer) ⇒
	 *       result.downPointers.contains(downPointer.webId.value) AND <br>
	 *       fold = fold.webId.value AND <br>
	 *       surrogateFold = surrogateFold.webId.value <br>
	 *       inverseSurrogateFold = inverseSurrogateFold.webId.value <br>
	 *
	 */

	public SimplifiedNodeDomain constructSimplifiedNodeDomain() {
		Node fold = connections.getFold();
		Node surrogateFold = connections.getSurrogateFold();
		Node inverseSurrogateFold = connections.getInverseSurrogateFold();

		//the getNeighbors is a little complicated I'm not sure exactly
		//how to itterate through one nodes neighbor connections.
		return new SimplifiedNodeDomain(webId.getValue(), getHeight(),
						getWebIds(connections.getNeighbors()), getWebIds(connections.getUpPointers()),
						getWebIds(connections.getDownPointers()), fold == null ? -1 : fold.getWebId().getValue(),
						surrogateFold == null ? -1 : surrogateFold.getWebId().getValue(),
						inverseSurrogateFold == null ? -1 : inverseSurrogateFold.getWebId().getValue(), 4);
	}

	/**
	 * Creates the set of webid's from the nodes passed in.
	 *
	 * @param nodes the list of nodes whose webIds are requested
	 * @return a HashSet of webIds
	 *
	 * @pre <i>None</i> <br>
	 * @post HashSet<Integer> x = y.getWebId().getValue() <br>
	 */
	public HashSet<Integer> getWebIds(HashSet<Node> nodes) {
		HashSet<Integer> returnMe = new HashSet<Integer>();

		for(Node n : nodes) {
			returnMe.add(n.getWebId().getValue());
		}
		return returnMe;
	}
	protected HashSet<Integer> getWebIds(ArrayList<Node> nodes) {
		HashSet<Integer> returnMe = new HashSet<Integer>();

		for(Node n : nodes) {
			returnMe.add(n.getWebId().getValue());
		}
		return returnMe;
	}

	/**
	 * Finds the greatest height out of all the neighbors.
	 *
	 * @return the height of the node
	 *
	 * @pre <i>None</i> <br>
	 * @post int greatestHeight = n.getWebId().getHeight();
	 */
	public int getHeight() {
		int greatestHeight = webId.getHeight();
		for(Node n : connections.getNeighbors()) {
			if(n.getWebId().getHeight() > greatestHeight) {
				greatestHeight = n.getWebId().getHeight();
			}
		}
		return greatestHeight;
	}

	/**
	 * The accessor for webId.
	 *
	 * @return webId
	 */
	public WebId getWebId() {
		return webId;
	}

	/**
	 * sets WebId.
	 *
	 * @param id sets this.webId to the given webId
	 *
	 * @pre <i>this.webId = null</i> <br>
	 * @post this.webId = id;
	 */
	public boolean setWebId(WebId id) {		
		this.webId = id;
		return true;

	}
	
	public boolean setMyNode(Node node) {
		connections.setMyNode(node);
		return true;

	}

	/**
	 * sets Fold.
	 *
	 * @param node sets fold to the given node
	 *
	 * @pre <i>fold = null</i> <br>
	 * @post this.fold = node;
	 */
	public boolean setFold(Node node) {
		connections.setFold(node);
		return true;

	}
	
	public boolean setNewFold(Node node) {
		connections.setNewFold(node);
		return true;

	}
	
	public boolean replaceFold(Node node) {
		connections.setFold(node);
		return true;
	}

	/**
	 * @return fold
	 *
	 * @pre <i>None</i>
	 * @post result = connections.getFold()
	 */
	public Node getFold() {
		return connections.getFold();
	}

	/**
	 * sets Surrogate Fold.
	 *
	 * @param node sets surrogate fold to the given node
	 *
	 * @pre <i>surrogateFold = null</i> <br>
	 * @post this.surrogateFold = node;
	 */
	public boolean setSurrogateFold(Node node) {
		connections.setSurrogateFold(node);
		return true;
	}

	public boolean setNewSurrogateFold(Node node) {
		connections.setNewSurrogateFold(node);
		return true;
	}
	
	/**
	 * sets Inverse Surrogate Fold.
	 *
	 * @param node sets inverse surrogate node to the given node
	 *
	 * @pre <i>inverseSurrogateFold = null</i> <br>
	 * @post this.inverseSurrogateFold = node;
	 */
	public boolean setInverseSurrogateFold(Node node) {
		connections.setInverseSurrogateFold(node);
		return true;
	}

	/**
	 * Adds the neighbors.
	 *
	 * @param node adds the given node to the nodes list of neighbors
	 *
	 * @pre <i>None</i> <br>
	 * @post node is now an element of Neighbors.
	 */
	public boolean addNeighbor(Node node) {
		connections.addNeighbor(new Neighbor(node));
		return true;

	}
	public boolean addNeighbor(NodeDelegate node) {
		connections.addNeighbor(node);
		return true;

	}

	/**
	 * removes Neighbor.
	 *
	 * @param node removes the given node from the nodes list of neighbors
	 *
	 * @pre <i>None</i> <br>
	 * @post node is now removed from the set neighbors.
	 */
	public boolean removeNeighbor(Node node) {
		connections.removeNeighbor(new Neighbor(node));
		return true;

	}
	public boolean removeNeighbor(NodeDelegate node) {
		connections.removeNeighbor(node);
		return true;

	}
	
	public boolean replaceNeighbor(NodeDelegate node, NodeDelegate replacingNode) {
		connections.replaceNeighbor(node, replacingNode);
		return true;
	}

	/**
	 * adds UpPointer.
	 *
	 * @param node adds the given node to the nodes list of up pointers
	 *
	 * @pre <i>None</i> <br>
	 * @post node is added to the set of upPointers.
	 */
	public boolean addUpPointer(Node node) {
		addUpPointer(new UpPointer(node));
		return true;

	}
	public boolean addUpPointer(NodeDelegate node) {
		connections.addUpPointer(node);
		return true;

	}

	/**
	 * removes upPointer.
	 *
	 * @param node removes the given node from the nodes list of up pointers
	 *
	 * @pre <i>None</i> <br>
	 * @post node is now removed from the set upPointers.
	 */
	public boolean removeUpPointer(Node node) {
		removeUpPointer(new UpPointer(node));
		return true;

	}
	public boolean removeUpPointer(NodeDelegate node) {
		connections.removeUpPointer(node);
		return true;

	}
	
	public boolean removeUpPointers() {
		connections.removeUpPointers();
		return true;
	}
	
	public boolean replaceUpPointer(NodeDelegate node, NodeDelegate replacingNode) {
		connections.replaceUpPointer(node, replacingNode);
		return true;

	}
	
	public boolean removeSmallerNeighborsChild() {
		connections.removeSmallerNeighborsChild();
		return true;
	}

	/**
	 * adds DownPointer.
	 *
	 * @param node adds the given node to the nodes list of down pointers
	 *
	 * @pre <i>None</i> <br>
	 * @post node is added to the set of downPointers.
	 */
	public boolean addDownPointer(Node node) {
		addDownPointer(new DownPointer(node));
		return true;

	}
	public boolean addDownPointer(NodeDelegate node) {
		connections.addDownPointer(node);
		return true;

	}

	/**
	 * removes DownPointer.
	 *
	 * @param node removes the given node from the nodes list of
	 * 			down pointers
	 *
	 * @pre <i>None</i> <br>
	 * @post node0 is now removed from the set downPointers.
	 */
	public boolean removeDownPointer(Node node) {
		removeDownPointer(new DownPointer(node));
		return true;
	}
	
	public boolean removeDownPointer(NodeDelegate node) {
		connections.removeDownPointer(node);
		return true;
	}
	
	public boolean replaceDownPointer(NodeDelegate node, NodeDelegate replacingNode) {
		connections.replaceDownPointer(node, replacingNode);
		return true;
	}

	public boolean equals(Object o) {
		int webId1;
		int webId2;
		
		webId1 = this.getWebId().getValue();
		webId2 = ((Node)o).getWebId().getValue();

		return webId1 == webId2;
	}
	
	/**
	 * Finds the smallest neighbor of the node. If the node is an edge node,
	 * this neighbor will be the parent of the node.
	 *
	 * @pre The node must be an edge node
	 * @post
	 *
	 * @return the parent node
	 */
	public Node getParent() {
		//The parent of the edge node is the node's smallest neighbor

		Node smallest = null;
		for (Node n : connections.getNeighbors()) {
			if (smallest == null) {
				smallest = n;
			}
			else {
				if (smallest.getWebId().getValue() > n.getWebId().getValue()) {
					smallest = n;
				}
			}
		}
		return smallest;
	}
	
	public Node getSmallestNeighborWithoutChildren() {
		return connections.getSmallestNeighborWithoutChildren();
	}

	public boolean addToHyPeerWeb(Node n) {
		new ConnectionsAdder().execute(n, this);
//		System.out.println(n.getWebId().getValue() + " " + n.getConnections());
		return true;
	}

	public boolean removeFromHyPeerWeb(Node n) {
        new ConnectionsReplacer().execute(n, new ConnectionsRemover().execute(n, this));
		ObjectDB.getSingleton().remove(n.getLocalObjectId());
		return true;
    }
	
	public boolean shiftNeighborsRight() {
		connections.shiftNeighborsRight();
		return true;

	}
	
	public boolean shiftNeighborsLeft() {
		connections.shiftNeighborsLeft();
		return true;
	}
	
	public Node find(Node node, WebId id) {
		return connections.find(node, id);
	}

	/* BROADCAST AND SEND METHODS*/
	public Node accept(Visitor visitor, Parameters parameters) {
		visitor.visit(this, parameters);
		return (Node)parameters.get("final_node");
	}
	public Contents getContents() {
		return contents;
	}
	
	public ArrayList<Node> getNeighborsToBroadcast() {
		return connections.getNeighborsForBroadcast(this);
	}
	
	public String toString() {
		return webId.getValue() + "loid:" + localObjectId + this.connections.toString();
		//return "";
	}
	
	public Object writeReplace() throws ObjectStreamException {
		if(migrating) {
			return this;
		}
		
		//change it to a nodeproxy with all my information
		GlobalObjectId global = HyPeerWeb.getSingleton().getGlobalObjectId();
		NodeProxy pNode = new NodeProxy(new GlobalObjectId(global.getMachineAddr(),global.getPortNumber(),this.localObjectId));
		pNode.changeWebId(this.webId);
		return pNode;
	}

	public LocalObjectId getLocalObjectId() {
		return localObjectId;
	}
	
	public void migrating(boolean m) {
		migrating = m;
	}

	public boolean updateLocalObjectId() {
		localObjectId = new LocalObjectId();
		ObjectDB.getSingleton().store(localObjectId, this);
		return true;
	}

	public boolean updateConnections(GlobalObjectId globalObjectId) {
		this.migrating(false);
		for(NodeDelegate n : this.connections.getAllConnections()) {
			//System.out.println("In For "+(n!=null?n.delegate.getWebId().getValue():"NULL"));
			if(n != null /*&& n.delegate instanceof NodeProxy*/) {
				//System.out.println("n!=null and n.delegate is node proxy: "+n.delegate.getWebId().getValue());

				if( HyPeerWeb.getSingleton().getNode(n.delegate.getWebId().getValue()) != null ) {
					System.out.println("Updating to real node");
					n.migrateNode(HyPeerWeb.getSingleton().getNode(n.delegate.getWebId().getValue()), this);
					/*if(n instanceof SurrogateFold) {
						this.setNewSurrogateFold(HyPeerWeb.getSingleton().getNode(n.delegate.getWebId().getValue()));
					}
					else if(n instanceof InverseSurrogateFold) {
						this.setInverseSurrogateFold(HyPeerWeb.getSingleton().getNode(n.delegate.getWebId().getValue()));
					}
					else {
						n.migrateNode(HyPeerWeb.getSingleton().getNode(n.delegate.getWebId().getValue()));
					}*/
					ObjectDB.getSingleton().dump();
				}

				n.delegate.replaceNodeProxyWithNode(this);
			}
		}
		
		return true;
	}
	
	public boolean replaceNodeProxyWithNode(Node node) {
		System.out.println("Replace Proxy With Node________________________________________ ");
		ObjectDB.getSingleton().dump();


		for(NodeDelegate n : this.connections.getAllConnections()) {
			if(n != null && n.delegate instanceof NodeProxy) {
				if(n.delegate.getWebId().getValue() == node.getWebId().getValue()) {
					System.out.println("Resetting " + this.getWebId().getValue() + " : " + n.getClass());
					System.out.println(n + "\n" + node);
					n.migrateNode(node, this);
				}
			}
		}
		ObjectDB.getSingleton().dump();
		System.out.println("END  Replace Proxy With Node________________________________________ ");

		return true;
	}
	
	public Node getSurrogateFold() {
		return connections.getSurrogateFold();
	}
	
	public ArrayList<Node> getUpPointers() {
		return connections.getUpPointers();
	}
	
	public ArrayList<Node> getLargerNeighbors() {
		ArrayList<Node> nodes = new ArrayList<Node>();
		for(NodeDelegate n : connections.getLargerNeighbors()) {
			if(n != null && n.delegate != null) {
				nodes.add(n.delegate);
			}
		}
		return nodes;
	}
	
	public ArrayList<Node> getSmallerNeighborsWithChildren() {
		ArrayList<Node> nodes = new ArrayList<Node>();
		for(NodeDelegate n : connections.getSmallerNeighborsWithChildren()) {
			if(n != null && n.delegate != null) {
				nodes.add(n.delegate);
			}
		}
		return nodes;
	}
	
	public ArrayList<Node> getSmallerNeighborsWithoutChildren() {
		ArrayList<Node> nodes = new ArrayList<Node>();
		for(NodeDelegate n : connections.getSmallerNeighborsWithoutChildren()) {
			if(n != null && n.delegate != null) {
				nodes.add(n.delegate);
			}
		}
		return nodes;
	}
	public void addSmallerWithNeighbor(Neighbor nD) {
		connections.addToSmallWithChildren(nD);
	}
	public void addSmallerWithoutNeighbor(Neighbor nD) {
		connections.addToSmallWithoutChildren(nD);
	}
	public void addLargerNeighbor(Neighbor nD) {
		connections.addToLargerNeighbors(nD);
	}
	public void addUpPointerDB(UpPointer nD) {
		connections.addUpPointerDB(nD);
	}
	public Node getInverseSurrogateFold() {
		return connections.getInverseSurrogateFold();
	}
	public void addDownPointerDB(DownPointer nD) {
		addDownPointer(nD);
	}
	public ArrayList<Node> getDownPointers() {
		return connections.getDownPointers();
	}
}
